-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[ty] Add initial implementation of goto definition for loads of local names #19123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
let index = semantic_index(model.db, model.file); | ||
let file_scope = index.expression_scope_id(*self); | ||
let scope = file_scope.to_scope_id(model.db, model.file); | ||
let use_def = index.use_def_map(file_scope); | ||
let use_id = self.scoped_use_id(model.db, scope); | ||
|
||
Some( | ||
use_def | ||
.bindings_at_use(use_id) | ||
.filter_map(|binding| binding.binding.definition()) | ||
.collect(), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically the happy path of TypeInferenceBuilder::infer_place_load
. It's extremely limited because it can't walk into parent scopes, and because it never engages type inference, it can't reason about "oh this is MyClass.myfield
".
I feel like the more Correct solution we want here is to augment infer_place_load
to also yield Definition info, and then run the whole TypeInferenceBuilder machinery just like inferred_type
does.
@@ -2667,7 +2667,7 @@ impl<'db> BindingError<'db> { | |||
if let Some(builder) = context.report_lint(&MISSING_ARGUMENT, node) { | |||
let s = if parameters.0.len() == 1 { "" } else { "s" }; | |||
let mut diag = builder.into_diagnostic(format_args!( | |||
"No argument{s} provided for required parameter{s} {parameters}{}", | |||
"No argument{s} provided for required parameterzz{s} {parameters}{}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops.
pub(crate) struct GotoDefinitionRequestHandler; | ||
|
||
impl RequestHandler for GotoDefinitionRequestHandler { | ||
type RequestType = GotoDefinition; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything in this file is a copy of goto_type_definition but changed to goto_definition
assert_snapshot!(test.goto_definition(), @r" | ||
info[goto-type-definition]: Type definition | ||
--> main.py:4:13 | ||
| | ||
2 | ab = 1 | ||
3 | ab = 2 | ||
4 | ab = 3 | ||
| ^^ | ||
5 | print(ab) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the first non-trivial property of the current implementation: the "definitions" we yield are the bindings that dominate the load. This is a useful answer but obviously not the only possible one.
2 | ab = 1 | ||
3 | if cond: | ||
4 | ab = 2 | ||
| ^^ | ||
5 | print(ab) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(in this case we point to both ab = 1
(above) and ab = 2
as both bindings can affect the load)
ab: int | ||
print(a<CURSOR>b) | ||
"#, | ||
); | ||
|
||
assert_snapshot!(test.goto_definition(), @"No definitions found"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case there's no bindings for ab
so we fail to yield anything (suboptimal).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be part of goto declarations instead :)
a<CURSOR>b += 2 | ||
print(ab) | ||
"#, | ||
); | ||
|
||
assert_snapshot!(test.goto_definition(), @"No goto target found"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any kind of Store completely falls over right now -- it seems like most of the code gives up when it sees Store, even if it's also a Load, but maybe it's handled by different paths? (in the playground ab += 2
does seem to support getting the type of ab
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, augmented assignments are both LOAD
and STORE
.
crates/ty_ide/src/goto.rs
Outdated
2 | class AB(val: int): | ||
| ^^ | ||
3 | self.myval = val | ||
| | ||
info: Source | ||
--> main.py:5:17 | ||
| | ||
3 | self.myval = val | ||
4 | | ||
5 | x = AB(5) | ||
| ^^ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this is only working because the class is defined in the same scope. If x = AB(5)
was in a function it would fail to resolve, because we don't look into parent scopes. yet.
x = AB(5) | ||
print(x.my<CURSOR>val) | ||
"#, | ||
); | ||
|
||
assert_snapshot!(test.goto_definition(), @"No goto target found"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is hopeless without type info.
RED = "red" | ||
BLUE = "blue" | ||
|
||
x = AB.RE<CURSOR>D |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly hopeless without type info
ab = 1 | ||
def myfunc(): | ||
global ab | ||
print(a<CURSOR>b) | ||
"#, | ||
); | ||
|
||
assert_snapshot!(test.goto_definition(), @"No definitions found"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Globals of course fail here without processing of parent scopes.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking this on! This seems like a good start but I'm a bit hesitant on shipping this based on the limitation that this only works if both the definition and usage are in the same scope.
The reason this limitation exists for goto definition and not goto type definition is because the former is only looking at the scope of the expression where it's being used while the latter is performing the type inference and fetching the list of definitions from Type
instead (Type::definition
). I think you're most likely aware of this already.
The reason it's fine for goto type definition is because we actually need the type definition which would either be in the stub file (if present) or the runtime file. This is the behavior of type inference in ty so it falls out naturally in the LSP.
For goto definition, I think we'll need something else. One way to solve this would be the following which is a bit high level:
- This is something that Eric mentioned -- create a source mapping between the definitions in runtime and stub file
- Use the same path as goto type definition but use the above mapping to get the corresponding runtime definition instead of the type definition
But to decrease the scope, we could probably ship a limited version which performs the same steps as goto type definition but filters out all the targets that would lead to a stub file and only return the ones that are present in the runtime files. This does mean that goto definition on any builtin symbols or the ones originating in the standard library wouldn't work because they all go to the stub files present in typeshed.
if snapshot | ||
.resolved_client_capabilities() | ||
.type_definition_link_support |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use the linkSupport
value from the DefinitionClientCapabilities
instead: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
You'll need to add a new definition_link_support
field in ResolvedClientCapabilities
which takes it's value from document.definition.link_support
. Refer to how type_definition_link_support
is being computed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm what distinguishes this from goto_type_definition, which has the same impl? (or is this a "also fix goto_type_definition"?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the client capabilities, so if a client has link support for goto type definition but not for goto definition (for whatever reason), the server needs to respect that. These capabilities are provided to the server during initialization where we make note of all the things that the client supports through which the server needs to make certain decisions. This is one of them.
) | ||
} | ||
ExprContext::Store => None, | ||
ExprContext::Del => None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can include ExprContext::Del
here as the following should go to the definition of x
:
def _():
x = 1
del x<CURSOR>
.collect(), | ||
) | ||
} | ||
ExprContext::Store => None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might a bit tricky because the definition that corresponds to this expression itself should also be included in the list of definitions. For example, here both definitions should be included:
def _():
x = 1
x<CURSOR> = 2
a<CURSOR>b += 2 | ||
print(ab) | ||
"#, | ||
); | ||
|
||
assert_snapshot!(test.goto_definition(), @"No goto target found"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, augmented assignments are both LOAD
and STORE
.
ExprContext::Load => { | ||
let index = semantic_index(model.db, model.file); | ||
let file_scope = index.expression_scope_id(*self); | ||
let scope = file_scope.to_scope_id(model.db, model.file); | ||
let use_def = index.use_def_map(file_scope); | ||
let use_id = self.scoped_use_id(model.db, scope); | ||
|
||
Some( | ||
use_def | ||
.bindings_at_use(use_id) | ||
.filter_map(|binding| binding.binding.definition()) | ||
.collect(), | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh hmm, I see what you mean based on what you mentioned on Discord. So, this implementation would be limited to only provide definitions if they're defined in the current scope, right? The following doesn't work:
x = 1
def _():
x<CURSOR>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes correct.
|
||
Some(RangedValue { | ||
range: FileRange::new(file, goto_target.range()), | ||
value: NavigationTargets::unique(targets), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we require to call unique
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For whatever reason it's the constructor that takes a list (the name was concerning, but, it works?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea behind using unique here is that, at least for go to type definition, we want to avoid showing the same definition twice. E.g. if you have a Class<str> | Class<int>
type. We only want to list one target because both those type point to the generic Class<T>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I think this makes sense for goto definitions as well.
@@ -29,6 +30,35 @@ pub fn goto_type_definition( | |||
}) | |||
} | |||
|
|||
pub fn goto_definition( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should split this into two files: goto_type_definition.rs
and goto_definition.rs
to isolate the logic and the tests. I'm not exactly sure where should the shared logic reside in, maybe the goto.rs
could be used for that but that sounds a bit confusing.
let source = targets.range; | ||
self.render_diagnostics( | ||
targets | ||
.into_iter() | ||
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to create a generic GotoDiagnostic
which takes in an enum that specifies which kind it is: GotoKind::Definition
, GotoKind::TypeDefinition
, GotoKind::Declaration
, etc.
No description provided.